import java.awt.Graphics2D;
import java.awt.geom.CubicCurve2D;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

public class Arrow extends Named implements Paintable, Serializable {
	protected Configuration config;
	protected Node fromNode, toNode;
	protected final Point2D fromPoint = new Point2D.Double(),
			toPoint = new Point2D.Double();
	protected double fromAngle, toAngle;
//	private boolean selected, hovered, held, indicated, highlighted, relationSelected;
	public boolean isBidirectional;
	private final Line2D.Double baseLine = new Line2D.Double();
	private final ArrowHead arrowHead;
	private final CubicCurve2D baseCurve = new CubicCurve2D.Double();

	public Arrow(Node fromNode, Node toNode) {
		this.fromNode = fromNode;
		this.toNode = toNode;
		config = Configuration.getDefaultConfiguration();
		arrowHead = new ArrowHead(config, this);
		setName();
		update();
	}

	public String toString() {
		return "Arrow " + name + " from " + fromNode + " to " + toNode;
	}

	public Node getToNode() {
		return toNode;
	}

	public void setToNode(Node node) {
		this.toNode = node;
	}

	public Node getFromNode() {
		return fromNode;
	}

	public void setFromNode(Node node) {
		this.fromNode = node;
	}

	public double getFromAngle() {
		return fromAngle;
	}

	public double getToAngle() {
		return toAngle;
	}

	public Point2D getFromPoint() {
		return fromPoint;
	}

	public Point2D getToPoint() {
		return toPoint;
	}

	@Override
	protected void calculateLabelPoint(double w, double h) {
		Point2D midpoint = getMidpoint();
		labelPoint
				.setLocation(midpoint.getX() - w / 2, midpoint.getY() - h / 2);
	}

	public void setSelected(boolean selected) {
		this.selected = selected;
	}

    /**
     * Updates the arrows graphical start point and end point, and at the same time updates the graphical points of
     * the arrows start and end node to ensure graphical consistency.
     * @param dx number of pixels to move in the horizontal direction
     * @param dy number of pixels to movi in the vertical direction
     */
	public void move(double dx, double dy) {
		Point2D oldPoint = fromNode.getPoint();
		fromNode.moveTo(oldPoint.getX() + dx, oldPoint.getY() + dy);
		if (fromNode != toNode) {
			oldPoint = toNode.getPoint();
			toNode.moveTo(oldPoint.getX() + dx, oldPoint.getY() + dy);
		}
	}

	public void setHeld(boolean held) {
		this.held = held;
	}

    /**
     * Changes this arrows direction, so it points from its current to node to its current from node.
     */
	public void flip() {
		Node tmp = fromNode;
		fromNode = toNode;
		toNode = tmp;
		update();
	}

	@Override
	public String generateName(String preferredName) {
		if (preferredName == null || preferredName.isEmpty()) {
			preferredName = config.defaultArrowName;
		}
		if (preferredName == name){
			return name;
		}
		Set<String> namePool = QuiverFrame.getInstance().getNamePool();
		String name = "";
		Set<Integer> used = new HashSet<Integer>();

		ArrayList<String> preferredNameParts = new ArrayList<String>(Arrays
				.asList(preferredName.split(",")));
		if (preferredName.endsWith(",")) {
			preferredNameParts.add("");
		}
		for (String preferredNamePart : preferredNameParts) {
			if (preferredNamePart.isEmpty()) {
				preferredNamePart = config.defaultArrowName;
			}
			int separation = digitSeparation(preferredNamePart);
			String tmp = preferredNamePart.substring(separation);
			int preferredIndex = -1;
			if (!tmp.isEmpty()) {
				preferredIndex = Integer.parseInt(tmp);
			}
			preferredNamePart = preferredNamePart.substring(0, separation);
			for (String usedName : QuiverFrame.getInstance().getNamePool()) {
				for (String namePart : usedName.split(",")) {
					separation = digitSeparation(namePart);
					tmp = namePart.substring(separation);
					int index = -1;
					if (!tmp.isEmpty()) {
						index = Integer.parseInt(tmp);
					}
					namePart = namePart.substring(0, separation);

					if (!namePart.equals(preferredNamePart)) {
						continue;
					}
					used.add(index);
				}
			}
			if (!used.contains(preferredIndex)) {
				if (preferredIndex == -1) {
					name += preferredNamePart + ",";
				} else {
					name += preferredNamePart + preferredIndex + ",";
				}
			} else {
				int i = 1;
				while (true) {
					if (!used.contains(i)) {
						name += preferredNamePart + i + ",";
						used.add(i);
						break;
					}
					i++;
				}
			}
			used.add(preferredIndex);
		}
		if (name.endsWith(",")) {
			name = name.substring(0, name.length() - 1);
		}
		return name;
	}

	private void adjustForNodeRadius() {
		fromPoint.setLocation(fromPoint.getX() + config.visibleNodeRadius
				* Math.cos(fromAngle), fromPoint.getY()
				+ config.visibleNodeRadius * Math.sin(fromAngle));
		toPoint.setLocation(toPoint.getX() - config.visibleNodeRadius
				* Math.cos(toAngle), toPoint.getY() - config.visibleNodeRadius
				* Math.sin(toAngle));
	}

	private void updateBidirectional() {
		double length = fromPoint.distance(toPoint) / 2;
		fromAngle = Math.atan2(toPoint.getY() - fromPoint.getY(), toPoint
				.getX()
				- fromPoint.getX());
		toAngle = fromAngle;

		if (config.bentBidirectionalArrows) {
			double bentArrowAngle = (90 / (1 + Math.pow(length, 2) * 0.0001))
					* Math.PI / 180;
			fromAngle += bentArrowAngle / 2;
			toAngle -= bentArrowAngle / 2;
		}

		adjustForNodeRadius();

		if (config.bentBidirectionalArrows) {
			length /= 2;
			Point2D.Double cp1 = new Point2D.Double(fromPoint.getX() + length
					* Math.cos(fromAngle), fromPoint.getY() + length
					* Math.sin(fromAngle));
			Point2D.Double cp2 = new Point2D.Double(toPoint.getX() + length
					* Math.cos(toAngle + Math.PI), toPoint.getY() + length
					* Math.sin(toAngle + Math.PI));
			baseCurve.setCurve(fromPoint, cp1, cp2, toPoint);
		} else {
			double dx = config.arrowSpacing / 2
					* Math.cos(fromAngle - Math.PI / 2);
			double dy = config.arrowSpacing / 2
					* Math.sin(fromAngle - Math.PI / 2);
			fromPoint.setLocation(fromPoint.getX() + dx, fromPoint.getY() + dy);
			toPoint.setLocation(toPoint.getX() + dx, toPoint.getY() + dy);
			baseLine.setLine(fromPoint, toPoint);
		}
	}

	private void updateLoop() {
		double angle = fromNode.calculateAvailableAngle(this);
		fromAngle = angle + config.arrowLoopAngle - Math.PI / 2;
		toAngle = angle - config.arrowLoopAngle - Math.PI / 2;

		adjustForNodeRadius();

		Point2D cp1 = new Point2D.Double(fromPoint.getX()
				+ config.arrowLoopSize * Math.cos(fromAngle), fromPoint.getY()
				+ config.arrowLoopSize * Math.sin(fromAngle));

		Point2D cp2 = new Point2D.Double(toPoint.getX() + config.arrowLoopSize
				* Math.cos(toAngle + Math.PI), toPoint.getY()
				+ config.arrowLoopSize * Math.sin(toAngle + Math.PI));

		baseCurve.setCurve(fromPoint, cp1, cp2, toPoint);
	}

	private void updateStraight() {
		fromAngle = Math.atan2(toPoint.getY() - fromPoint.getY(), toPoint
				.getX()
				- fromPoint.getX());
		toAngle = fromAngle;
		adjustForNodeRadius();
		baseLine.setLine(fromPoint, toPoint);
	}

    /**
     * Updates this arrows graphical components to ensure graphical consistency. Used when updating either its from
     * node or to node.
     */
	public void update() {
		fromPoint.setLocation(fromNode.getPoint());
		toPoint.setLocation(toNode.getPoint());
		if (isBidirectional) {
			updateBidirectional();
		} else if (isLoop()) {
			updateLoop();
		} else {
			updateStraight();
		}
		arrowHead.update();
	}

	/**
	 * Based on formula from
	 * http://en.wikipedia.org/wiki/Bézier_curve#Cubic_B.C3.A9zier_curves Call
	 * with t=0.5 to get the midpoint of the curve.
	 *
	 * @param curve
	 * @param t
	 *            in the range [0,1]
	 * @return the point on the cubic curve given by a parameter t
	 */
	private Point2D getCubicCurvePoint(CubicCurve2D curve, double t) {
		return new Point2D.Double(Math.pow(1 - t, 3) * curve.getX1() + 3
				* Math.pow(1 - t, 2) * t * curve.getCtrlX1() + 3 * (1 - t)
				* Math.pow(t, 2) * curve.getCtrlX2() + Math.pow(t, 3)
				* curve.getX2(), Math.pow(1 - t, 3) * curve.getY1() + 3
				* Math.pow(1 - t, 2) * t * curve.getCtrlY1() + 3 * (1 - t)
				* Math.pow(t, 2) * curve.getCtrlY2() + Math.pow(t, 3)
				* curve.getY2());
	}

    /**
     * This method finds the point on the middle of this arrows line. Used when positioning the arrows label.
     * @return The midpoint of this arrows line
     */
	public Point2D getMidpoint() {
		if (isLoop() || isBidirectional && config.bentBidirectionalArrows) {
			return getCubicCurvePoint(baseCurve, 0.5);
		} else {
			return new Point2D.Double(
					0.5 * (fromPoint.getX() + toPoint.getX()), 0.5 * (fromPoint
							.getY() + toPoint.getY()));
		}
	}

	public boolean isLoop() {
		return fromNode == toNode;
	}

	public void paint(Graphics2D g){
		paint(g, null);
	}

	public void paint(Graphics2D g, Representation repr) {
        if (relationSelected) {
            g.setPaint(config.relationSelectedArrowColor);
        } else {
            g.setPaint(config.arrowColor);
        }

		arrowHead.paint(g);
		if (isLoop() || isBidirectional && config.bentBidirectionalArrows) {
			g.draw(baseCurve);
		} else {
			g.draw(baseLine);
		}
		if (config.showLabels) {
			if (repr != null){
				String[][] map = repr.getMap(this);
				if (Representation.isZeroMap(map)){
					paintLabel(g, Representation.ZERO);
				} else{
					paintLabel(g, map.length + "x" + map[0].length);
				}
			} else{
				paintLabel(g);
			}
		}
	}

	public boolean isClose(Point2D point) {
		if (isLoop()) {
			Point2D midpoint = getMidpoint();
			double centerX = 0.5 * (fromPoint.getX() + midpoint.getX());
			double centerY = 0.5 * (fromPoint.getY() + midpoint.getY());
			double radius = midpoint.distance(centerX, centerY);
			double dist = point.distance(centerX, centerY);
			return dist > radius - config.arrowWidth
					&& dist < radius + config.arrowWidth;
		} else if (isBidirectional && config.bentBidirectionalArrows) {
			// TODO: write this
			return false;
		} else {
			return baseLine.ptSegDist(point) <= config.arrowWidth / 2;
		}
	}
}
